1

本篇内容为《机器学习实战》第 4 章 基于概率论的分类方法:朴素贝叶斯程序清单。所用代码为 python3。


朴素贝叶斯

优点:在数据较少的情况下仍然有效,可以处理多类别问题。
缺点:对于输入数据的准备方式较为敏感。
适用数据类型:标称型数据。


使用 Python 进行文本分类

简单描述这个过程为:从文本中获取特征,构建分类器,进行分类输出结果。这里的特征是来自文本的词条 (token),需要将每一个文本片段表示为一个词条向量,其中值为 1 表示词条出现在文档中,0 表示词条未出现。

接下来给出将文本转换为数字向量的过程,然后基于这些向量来计算条件概率,并在此基础上构建分类器。

下面我们以在线社区的留言板为例,给出一个用来过滤的例子。
为了不影响社区的发展,我们需要屏蔽侮辱性的言论,所以要构建一个快速过滤器,如果某条留言使用来负面或者侮辱性的语言,就将该留言标识为内容不当。对此问题建立两个类别:侮辱类和非侮辱类,分别使用 1 和 0 来表示。

准备数据:从文本中构建词向量

程序清单 4-1 词表到向量的转换函数

'''
Created on Sep 10, 2018

@author: yufei
'''

# coding=utf-8
from numpy import *

# 创建一些实例样本
def loadDataSet():
    postingList = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
                 ['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
                 ['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'],
                 ['stop', 'posting', 'stupid', 'worthless', 'garbage'],
                 ['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
                 ['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
    classVec = [0,1,0,1,0,1]    # 1 代表侮辱性文字,0 代表正常言论

    """
    变量 postingList 返回的是进行词条切分后的文档集合。
    留言文本被切分成一些列词条集合,标点符号从文本中去掉
    变量 classVec 返回一个类别标签的集合。
    这些文本的类别由人工标注,标注信息用于训练程序以便自动检测侮辱性留言。
    """
    return postingList, classVec

"""
创建一个包含在所有文档中出现的不重复词的列表
是用python的 Set 数据类型
将词条列表输给 Set 构造函数,set 就会返回一个不重复词表
"""
def createVocabList(dataSet):
    # 创建一个空集合
    vocabSet = set([])
    # 将每篇文档返回的新词集合添加进去,即创建两个集合的并集
    for document in dataSet:
        vocabSet = vocabSet | set(document)
    # 获得词汇表
    return list(vocabSet)

# 参数:词汇表,某个文档
def setOfWords2Vec(vocabList, inputSet):
    # 创建一个和词汇表等长的向量,将其元素都设置为 0
    returnVec = [0] * len(vocabList)
    # 遍历文档中所有单词
    for word in inputSet:
        # 如果出现词汇表中的单词,将输出的文档向量中的对应值设为 1
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1
        else:
            print('the word: %s is not in my Vocabulary!' % word)
    # 输出文档向量,向量元素为 1 或 0
    return returnVec

在 python 提示符下,执行代码并得到结果:

>>> import bayes
>>> list0Posts, listClasses = bayes.loadDataSet()
>>> myVocabList = bayes.createVocabList(list0Posts)
>>> myVocabList
['problems', 'mr', 'ate', 'buying', 'not', 'garbage', 'how', 'maybe', 'stupid', 'cute', 'stop', 'help', 'dalmation', 'take', 'is', 'worthless', 'him', 'flea', 'park', 'my', 'I', 'to', 'licks', 'steak', 'dog', 'love', 'quit', 'so', 'please', 'posting', 'has', 'food']

即可得到的一个不会出现重复单词的词表myVocabList,目前该词表还没有排序。

继续执行代码:

>>> bayes.setOfWords2Vec(myVocabList, list0Posts[3])
[0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
>>> bayes.setOfWords2Vec(myVocabList, list0Posts[0])
[0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0]

函数setOfWords2Vec使用词汇表或者说想要检查的所有单词作为输入,然后为其中每一个单词构建一个特征。一旦给定一篇文章(本例中指一条留言),该文档就会被转换为词向量。

训练算法:从词向量计算概率

函数伪代码如下:

··· 计算每个类别中的文档数目
··· 对每篇训练文档:
······ 对每个类别:
········· 如果词条出现在文档中—>增加该词条的计数值
········· 增加所有词条的计数值
······ 对每个类别:
········· 对每个词条:
············ 将该词条对数目除以总词条数目得到条件概率
······ 返回每个类别对条件概率

程序清单 4-2 朴素贝叶斯分类器训练函数

'''
Created on Sep 11, 2018

@author: yufei
'''
# 参数:文档矩阵 trainMatrix,每篇文档的类别标签所构成的向量 trainCategory
def trainNB0(trainMatrix, trainCategory):
    numTrainDocs = len(trainMatrix) #文档的个数
    numWords = len(trainMatrix[0])  #获取第一篇文档的单词长度

    """
    计算文档属于侮辱性文档的概率
    用类别为1的个数除以总篇数
    sum([0,1,0,1,0,1])=3,也即是 trainCategory 里面 1 的个数
    """
    pAbusive = sum(trainCategory) / float(numTrainDocs)

    """
    初始化概率
    当利用贝叶斯分类器对文档分类时,计算多个概率的乘积以获得属于某个类别的概率
    把所有词出现次数初始化为1,分母初始化为2,用log避免数太小被约掉
    """
    p0Num = ones(numWords)
    p1Num = ones(numWords)

    p0Denom = 2.0
    p1Denom = 2.0

    # 遍历训练集 trainMatrix 中的所有文档
    for i in range(numTrainDocs):
        # 侮辱性词语在某个文档中出现
        if trainCategory[i] == 1:
            # 该词对应个数加一,即分子把所有的文档向量按位置累加
            # trainMatrix[2] = [1,0,1,1,0,0,0];trainMatrix[3] = [1,1,0,0,0,1,1]
            p1Num += trainMatrix[i]
            # 文档总词数加一,即对于分母
            # 把trainMatrix[2]中的值先加起来为3,再把所有这个类别的向量都这样累加起来,这个是计算单词总数目
            p1Denom += sum(trainMatrix[i])
        # 正常词语在某个文档中出现,同上
        else:
            p0Num += trainMatrix[i]
            p0Denom +=sum(trainMatrix[i])

    """
    对每个元素除以该类别的总词数,得条件概率
    防止太多的很小的数相乘造成下溢。对乘积取对数
    # p1Vect = log(p1Num / p1Denom)
    # p0Vect = log(p0Num / p0Denom)
    """
    
    p1Vect = p1Num / p1Denom
    p0Vect = p0Num / p0Denom

    """
    函数返回两个向量和一个概率
    返回每个类别的条件概率,是一个向量
    在向量里面和词汇表向量长度相同
    每个位置代表这个单词在这个类别中的概率
    """
    return p0Vect, p1Vect, pAbusive

在 python 提示符下,执行代码并得到结果:

>>> from numpy import *
>>> importlib.reload(bayes)
<module 'bayes' from '/Users/Desktop/Coding/bayes.py'>
>>> list0Posts, listClasses = bayes.loadDataSet()
>>> myVocabList = bayes.createVocabList(list0Posts)

以上,调入数据后构建了一个包含所有词的列表myVocabList

>>> trainMat = []
>>> for postinDoc in list0Posts:
...     trainMat.append(bayes.setOfWords2Vec(myVocabList, postinDoc))

这个for循环使用词向量来填充trainMat列表。

继续给出属于侮辱性文档的概率以及两个类别的概率向量。

>>> p0V, p1V, pAb = bayes.trainNB0(trainMat, listClasses)

查看变量的内部值

>>> pAb
0.5
>>> p0V
array([0.03846154, 0.07692308, 0.03846154, 0.07692308, 0.07692308,
       0.07692308, 0.07692308, 0.03846154, 0.03846154, 0.03846154,
       0.07692308, 0.07692308, 0.15384615, 0.07692308, 0.07692308,
       0.07692308, 0.03846154, 0.07692308, 0.07692308, 0.07692308,
       0.07692308, 0.07692308, 0.03846154, 0.07692308, 0.11538462,
       0.07692308, 0.07692308, 0.03846154, 0.03846154, 0.03846154,
       0.07692308, 0.03846154])
>>> p1V
array([0.0952381 , 0.04761905, 0.0952381 , 0.0952381 , 0.14285714,
       0.04761905, 0.04761905, 0.0952381 , 0.0952381 , 0.14285714,
       0.04761905, 0.04761905, 0.04761905, 0.04761905, 0.04761905,
       0.04761905, 0.0952381 , 0.04761905, 0.04761905, 0.04761905,
       0.0952381 , 0.04761905, 0.0952381 , 0.04761905, 0.0952381 ,
       0.04761905, 0.04761905, 0.19047619, 0.0952381 , 0.0952381 ,
       0.04761905, 0.0952381 ])

我们发现文档属于侮辱类的概率pAb为 0.5,查看pV1的最大值 0.19047619,它出现在第 27 个下标位置,查看myVocabList的第 27 个下标位置该词为 stupid,说明这是最能表征类别 1 的单词。

测试算法:根据现实情况修改分类器

程序清单 4-3 朴素贝叶斯分类函数

'''
Created on Sep 11, 2018

@author: yufei
'''
# vec2Classify: 要分类的向量
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    p1 = sum(vec2Classify * p1Vec) + log(pClass1)
    p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1)
    if p1 > p0:
        return 1
    else:
        return 0

def  testingNB():
    list0Posts, listClasses = loadDataSet()
    myVocabList = createVocabList(list0Posts)
    trainMat = []
    for posinDoc in list0Posts:
        trainMat.append(setOfWords2Vec(myVocabList, posinDoc))
    p0V, p1V, pAb = trainNB0(array(trainMat), array(listClasses))

    testEntry = ['love', 'my','dalmation']
    thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
    print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))
    testEntry = ['stupid', 'garbage']
    thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
    print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))

在 python 提示符下,执行代码并得到结果:

>>> importlib.reload(bayes)
<module 'bayes' from '/Users/Desktop/Coding/bayes.py'>
>>> bayes.testingNB()
['love', 'my', 'dalmation'] classified as:  0
['stupid', 'garbage'] classified as:  1

分类器输出结果,分类正确。

准备数据:文档词袋模型

词集模型:将每个词的出现与否作为一个特征。即我们上面所用到的。
词袋模型:将每个词出现次数作为一个特征。每遇到一个单词,其词向量对应值 +1,而不是全设置为 1。

对函数setOfWords2Vec()进行修改,修改后的函数为bagOfWords2VecMN

程序清单 4-4 朴素贝叶斯词袋模型

def bagOfWords2VecMN(vocabList, inputSet):
    returnVec = [0] * len(vocabList)
    for word in inputSet:
        if word in inputSet:
            returnVec[vocabList.index(word)] += 1
    return returnVec

修改的地方为:每当遇到一个单词时,它会增加词向量中的对应值,而不只是将对应的数值设为 1。

下面我们将利用该分类器来过滤垃圾邮件。


示例:使用朴素贝叶斯过滤垃圾邮件

测试算法:使用朴素贝叶斯进行交叉验证

程序清单 4-5 文件解析及完整的垃圾邮件测试函数

'''
Created on Sep 11, 2018

@author: yufei
'''

"""
接受一个大字符串并将其解析为字符串列表
"""
def textParse(bigString):    #input is big string, #output is word list
    import re
    listOfTokens = re.split(r'\W*', bigString)
    # 去掉小于两个字符的字符串,并将所有字符串转换为小写
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]

"""
对贝叶斯垃圾邮件分类器进行自动化处理
"""
def spamTest():
    docList=[]; classList = []; fullText =[]
    #导入并解析文本文件为词列表
    for i in range(1,26):
        wordList = textParse(open('email/spam/%d.txt' % i, encoding='ISO-8859-1').read())
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(1)
        
        wordList = textParse(open('email/ham/%d.txt' % i, encoding='ISO-8859-1').read())
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)
    vocabList = createVocabList(docList)#create vocabulary
    trainingSet = list(range(50)); testSet=[]           #create test set
    
    for i in range(10):
        randIndex = int(random.uniform(0,len(trainingSet)))
        testSet.append(trainingSet[randIndex])
        del(trainingSet[randIndex])
        
    trainMat=[]; trainClasses = []
    
    # 遍历训练集的所有文档,对每封邮件基于词汇表并使用 bagOfWords2VecMN 来构建词向量
    for docIndex in trainingSet:#train the classifier (get probs) trainNB0
        trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
        trainClasses.append(classList[docIndex])
    # 用上面得到的词在 trainNB0 函数中计算分类所需的概率
    p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
    errorCount = 0
    
    # 对测试集分类
    for docIndex in testSet:        #classify the remaining items
        wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
        # 如果邮件分类错误,错误数加 1 
        if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
            errorCount += 1
            print ("classification error",docList[docIndex])
    # 给出总的错误百分比
    print ('the error rate is: ',float(errorCount)/len(testSet))
    #return vocabList,fullText

在 python 提示符下,执行代码并得到结果:

>>> importlib.reload(bayes)
<module 'bayes' from '/Users/Desktop/Coding/bayes.py'>
>>> bayes.spamTest()
classification error ['home', 'based', 'business', 'opportunity', 'knocking', 'your', 'door', 'don', 'rude', 'and', 'let', 'this', 'chance', 'you', 'can', 'earn', 'great', 'income', 'and', 'find', 'your', 'financial', 'life', 'transformed', 'learn', 'more', 'here', 'your', 'success', 'work', 'from', 'home', 'finder', 'experts']
the error rate is:  0.1

函数spamTest()会输出在 10 封随机选择的电子邮件上的分类错误率。由于是随机选择的,所以每次的输出结果可能有些差别。如果想要更好地估计错误率,那么就应该将上述过程重复多次求平均值。

这里的代码需要注意的两个地方是:

1、直接使用语句 wordList = textParse(open('email/spam/%d.txt' % i).read()) 报错 UnicodeDecodeError: 'utf-8' codec can't decode byte 0x92 in position 884: invalid start byte。这是因为在文件里可能存在不是以 utf-8 格式保存的字符,需改为wordList = textParse(open('email/spam/%d.txt' % i, encoding='ISO-8859-1').read())

2、将随机选出的文档添加到测试集后,要同时将其从训练集中删除,使用语句 del(trainingSet[randIndex]),此时会报错 TypeError: 'range' object doesn't support item deletion,这是由于 python2 和 python3 的不同而导致的。在 python2 中可以直接执行,而在 python3 中需将 trainingSet 设为 trainingSet = list(range(50)),而不是 trainingSet = range(50),即必须让它是一个 list 再进行删除操作。


以上,我们就用朴素贝叶斯对文档进行了分类。


参考链接:
《机器学习实战》笔记之四——基于概率论的分类方法:朴素贝叶斯
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x92 in position 884: invalid start byte

不足之处,欢迎指正。


秋刀鱼
266 声望66 粉丝

做一件事最重要的是开心🏊